Linux 管道 IPC:匿名管道与有名管道
管道是 UNIX 系统最古老的进程间通信机制:ls | wc -l 里的 | 就是一根管道。本文写给写 Linux 多进程程序的开发者,讲清两类管道的区别与各自的正确用法——匿名管道(pipe)为什么只能给亲缘进程用、有名管道(FIFO)如何让任意进程通信、读写端各种关闭组合下会发生什么。读完能避开管道使用中最常见的几个坑。
进程间通信的方式
进程是独立的资源分配单元,不同进程之间不能直接访问对方的资源,需要内核提供通信机制。Linux 的 IPC 手段按来源分三类:
消息队列、共享内存、信号量同时有 System V 与 POSIX 两套接口(共享内存与内存映射见 Linux 共享内存与内存映射,异步通知见 Linux 信号机制:产生、阻塞与捕捉)。
匿名管道的特点
- 管道是内核内存中维护的缓冲区,容量有限,不同操作系统大小不一定相同
- 管道拥有文件的特质(读、写操作),匿名管道没有文件实体,有名管道有文件实体但不存储数据
- 管道是一个字节流:不存在消息边界,读取方每次读多少字节都合法,写入的数据块可能被合并
- 数据严格按写入顺序读出,读走即消失,无法用
lseek()随机访问 - 数据单向流动,一端读一端写,管道是半双工的
- 匿名管道只能用于有公共祖先的进程(父子、兄弟进程)——它靠 fork 时描述符的继承传递,没有名字可供无关进程打开
一个容易混淆的概念:fpathconf(fd, _PC_PIPE_BUF) 查询的 PIPE_BUF 是单次写入保证原子的字节上限(Linux 通常为 4096),不是管道的总容量;Linux 上总容量用 fcntl(fd, F_GETPIPE_SZ) 查询,自内核 2.6.11 起默认为 65536 字节。
基本用法
#include <unistd.h>
int pipe(int pipefd[2]);
// pipefd[0] 是读端,pipefd[1] 是写端;成功返回 0,失败返回 -1 pipe() 在 fork 之前调用,父子进程于是持有同一根管道的两端。习惯上各自关闭不用的那一端(父关写端、子关读端),这一点不只是省资源,直接决定了后面的读写行为:
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int pipefd[2];
int ret = pipe(pipefd); // 先建管道
if (ret == -1) {
perror("pipe");
exit(0);
}
pid_t pid = fork(); // 再 fork
if (pid > 0) {
// 父进程:关闭写端,只读
close(pipefd[1]);
char buf[1024] = {0};
while (1) {
int len = read(pipefd[0], buf, sizeof(buf));
if (len == 0) { // 写端已全部关闭且数据读完:读到 EOF
break;
}
printf("parent recv: %s\n", buf);
}
} else if (pid == 0) {
// 子进程:关闭读端,只写
close(pipefd[0]);
char *str = "hello, i am child";
while (1) {
write(pipefd[1], str, strlen(str));
sleep(1);
}
}
return 0;
} 管道的读写规则
阻塞模式下,读端与写端的关闭状态组合出四种行为,这是管道使用中最需要记住的一张表:
| 场景 | 行为 |
|---|---|
| 写端全部关闭,读端读 | 剩余数据读完后 read 返回 0(类似读到文件末尾) |
| 写端还有进程持有,但不写数据 | 数据读完后 read 阻塞,直到有新数据 |
| 读端全部关闭,写端写 | 写进程收到 SIGPIPE 信号,默认终止进程 |
| 读端还有进程持有,但不读数据 | 管道写满后 write 阻塞,直到腾出空间 |
两个实践推论:
- 判断「对方是否还活着」看 read 是否返回 0;写数据前如果不确定读端存在,要么装 SIGPIPE 处理器,要么用
send/带MSG_NOSIGNAL的接口替代场景,避免进程被信号杀掉。 - 读到 EOF(返回 0)后应该退出读循环,继续 read 只会无限返回 0 造成忙等。
非阻塞模式
对读端描述符设置 O_NONBLOCK,read 在无数据时立即返回 -1 并置 errno 为 EAGAIN,而不是挂起:
int flags = fcntl(pipefd[0], F_GETFL);
flags |= O_NONBLOCK;
fcntl(pipefd[0], F_SETFL, flags); 有名管道(FIFO)
匿名管道没有名字,无法被无关进程打开。有名管道(FIFO)在文件系统中创建一个特殊文件,任何能访问该路径的进程都可以打开它通信:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
// 成功返回 0,失败返回 -1;也可用命令 mkfifo(1) 创建 FIFO 与匿名管道的 I/O 语义完全一致(字节流、半双工、同样的读写规则),区别只在两点:它在文件系统中有名字、有实体,进程退出后 FIFO 文件仍在;因此没有亲缘关系的进程也能通过路径找到同一根管道。
FIFO 的打开阻塞
FIFO 必须两端都打开才能传数据。默认(阻塞模式)下,open(O_RDONLY) 会阻塞到有进程以写方式打开,open(O_WRONLY) 反之亦然。
设置 O_NONBLOCK 后行为不对称:只读打开立即成功(即使尚无写端),只写打开在尚无读端时失败并返回 ENXIO。
写进程与读进程的完整示例:
// 写进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
// 不存在则创建
int ret = access("test", F_OK);
if (ret == -1) {
ret = mkfifo("test", 0664);
if (ret == -1) {
perror("mkfifo");
exit(0);
}
}
int fd = open("test", O_WRONLY); // 阻塞至读端打开
if (fd == -1) {
perror("open");
exit(0);
}
for (int i = 0; i < 100; i++) {
char buf[1024];
sprintf(buf, "hello, %d\n", i);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
} // 读进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("test", O_RDONLY); // 阻塞至写端打开
if (fd == -1) {
perror("open");
exit(0);
}
while (1) {
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
if (len == 0) { // 写端全部关闭:EOF
printf("writer closed\n");
break;
}
printf("recv: %s", buf);
}
close(fd);
return 0;
} 用两条 FIFO 实现双工聊天
单根管道是单向的,双向聊天需要两根 FIFO,各进程再用一对父子或线程分别负责读与写:
客户端代码(另一端只需互换两条 FIFO 的读写方向):
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
// 两条管道不存在则创建
if (access("fifo1", F_OK) == -1 && mkfifo("fifo1", 0664) == -1) {
perror("mkfifo");
exit(0);
}
if (access("fifo2", F_OK) == -1 && mkfifo("fifo2", 0664) == -1) {
perror("mkfifo");
exit(0);
}
int fdw = open("fifo1", O_WRONLY);
int fdr = open("fifo2", O_RDONLY);
if (fdw == -1 || fdr == -1) {
perror("open");
exit(0);
}
pid_t pid = fork();
char buf[128];
if (pid > 0) {
// 父进程:标准输入 → fifo1
while (1) {
memset(buf, 0, 128);
fgets(buf, 128, stdin);
if (write(fdw, buf, strlen(buf)) == -1) {
perror("write");
break;
}
}
} else if (pid == 0) {
// 子进程:fifo2 → 标准输出
while (1) {
memset(buf, 0, 128);
int ret = read(fdr, buf, 128);
if (ret <= 0) {
break;
}
printf("recv: %s", buf);
}
}
close(fdw);
close(fdr);
return 0;
} 要点
- 匿名管道靠 fork 继承描述符,只适用于亲缘进程;FIFO 有路径名,任意进程可用
- 管道是半双工字节流,读走即消失;
PIPE_BUF是原子写上限而非容量,容量用fcntl(F_GETPIPE_SZ)查 - 四条读写规则:写端全关读返回 0,读端全关写触发 SIGPIPE,对端持有但不动作则阻塞
- FIFO 打开会阻塞到对端打开,
O_NONBLOCK下只读与只写的行为不对称 - 双向通信用两根管道,单根管道内不要试图双向传数据